(** Bombs-Must-Detonate: BMDLang Type Checker
    @author Brian Go *)

type bmdtype = 
    TyInt
  | TyFloat
  | TyString
  | TyBool
  | TyVoid
  | TyList of bmdtype
  | TyEnumType of string
  | TyEnum of string
  | TyStruct of string
  | TyArray of bmdtype
  | TyArrow of bmdtype list * bmdtype
  | TyRef of bmdtype
  | TyTemplate of string

(** Used to keep track of the typing environment during typechecking *)
type typeenvdef =
    TyEnumDef of string * string list
  | TyStructDef of string * (string * bmdtype) list * Ast.data_type list
  | TyFunDef of string * bmdtype
  | TyVarDef of string * bmdtype
  | TyTemplateDef of string list
  | TyIncludeDef of string

exception Type_error of string

(** Eliminates duplicate entries in a list *)
let rec elim_repeats lst = match lst with
    cur::rest when List.mem cur rest -> elim_repeats rest
  | cur::rest -> cur::(elim_repeats rest)
  | [] -> []


(** Linker header information buffer *)
let link_buffer = ref []

(** Clears the linker header information buffer *)
let reset_link_buffer () = link_buffer := []

(** Gets the contents of the linker header infromation *)
let get_link_buffer () = String.concat "" ((elim_repeats !link_buffer)@["END_HEADER\n"])

(** Appends a string to the linker buffer *)
let append_link_buffer str = 
  link_buffer := str::!link_buffer

(** Raises a type error from the concatenated string list *)
let raise_type_error s_list =
  let str = String.concat "" s_list in
    raise (Type_error str)

(** Renaming of String.concat "" *)
let concat s = String.concat "" s

(** Concatenates a list with commas.  No comma after the final element. *)
let rec concat_commas slist = 
  match slist with 
      x::[] -> x
    | x::rest -> concat [x;", ";concat_commas rest]
    | [] -> ""

(** Replaces an association list by a list of keys *)
let get_first lst = 
  let helper (x,y) = x in
    List.map helper lst

(** Replaces an association list by a list of values *)
let get_second lst = 
  let helper (x,y) = y in
    List.map helper lst

(** Checks if the given struct name is defined in the environment *)
let rec isdef_struct name env = 
  match env with
      (TyStructDef (s,_,_))::rest -> ((s = name) || isdef_struct name rest)
    | _::rest -> isdef_struct name rest
    | [] -> false

(** Checks if the given enum is defined in the environment *)
let rec isdef_enum name env = 
  match env with
      (TyEnumDef (s,_))::rest -> ((s = name) || isdef_enum name rest)
    | _::rest -> isdef_enum name rest
    | [] -> false

(** Checks if the given variable is defined in the environment *)
let rec isdef_var name env =
  match env with
      (TyVarDef (s,_))::rest -> ((s = name) || isdef_var name rest)
    | _::rest -> isdef_var name rest
    | [] -> false

(** Checks if the given function is defined in the environment *)
let rec isdef_fun name env = 
  match env with
      (TyFunDef (s,_))::rest -> ((s = name) || isdef_fun name rest)
    | _::rest -> isdef_fun name rest
    | [] -> false

(** Checks if the given template definition is defined in the environment *)
let rec isdef_templated name env = 
  match env with
      (TyTemplateDef slist)::rest -> List.mem name slist
    | _::rest -> isdef_templated name rest
    | [] -> false

(** Removes the most recent template definition from the environment *)
let rec remove_template_def env = 
  match env with
      (TyTemplateDef _) :: rest -> rest
    | cur :: rest -> cur :: (remove_template_def rest)
    | [] -> []

(** Checks if the given identifier is defined as a struct, enum, variable, or function in the environment *)
let rec isdef_ident name env = 
  isdef_struct name env
  || isdef_enum name env
  || isdef_var name env
  || isdef_fun name env


(** Gets the type of the given variable from the environment.  Raises Not_found if not defined.*)
let rec gettype_var name env = 
  match env with
      (TyVarDef (s,t))::rest when (s = name) -> t
    | _::rest -> gettype_var name rest
    | [] -> raise Not_found

(** Gets the type of the given function from the environment. Raises Not_found if not defined. *)
let rec gettype_fun name env = 
  match env with
      (TyFunDef (s,t))::rest when (s = name) -> t
    | _::rest -> gettype_fun name rest
    | [] -> raise Not_found

(** Checks if s2 is an enum value of enum type s1.  Raises Not_found if s1 is not an enum type. *)
let rec is_enum_value s1 s2 env =
  match env with 
      (TyEnumDef (s,lst))::rest when (s = s1) -> List.mem s2 lst
    | _::rest -> is_enum_value s1 s2 rest
    | [] -> raise Not_found

(** Checks if file "name" has been included in the given environment *)
let rec isdef_include name env = 
  match env with
      (TyIncludeDef s)::rest when (s = name) -> true
    | _::rest -> isdef_include name rest
    | [] -> false

(** Finds the index of a value in a list.  Raises Not_found if it is not in the list *)
let rec indexof str lst index =
  match lst with
      cur::rest when cur = str -> index
    | _::rest -> indexof str rest (index+1)
    | [] -> raise Not_found

(** Casts enum value s1.s2 to an integer.  Raises Not_found if s1.s2 is not defined *)
let rec get_enum_value s1 s2 env = 
  match env with 
      (TyEnumDef (s,lst))::rest when (s = s1) -> indexof s2 lst 0
    | _::rest -> get_enum_value s1 s2 rest
    | [] -> raise Not_found

(** Gets the total number of values of enum type ename.  Raises Not_found if ename is not an enum type *)
let rec get_nvalues_enum ename env = 
  match env with
      (TyEnumDef (s,lst))::rest when (s = ename) -> List.length lst
    | _::rest -> get_nvalues_enum ename rest
    | [] -> raise Not_found

(** Checks if s2 is a field of struct s1 from the environment.  Raises Not_found if s1 is not a struct *)
let rec is_struct_value s1 s2 env = 
  match env with 
      (TyStructDef (s,lst,_))::rest when (s = s1) -> List.mem s2 (get_first lst)
    | _::rest -> is_struct_value s1 s2 rest
    | [] -> raise Not_found

(** Gets the data type of field s2 of struct s1 from the environment.  Raises Not_found if s1 is not a struct *)
let rec gettype_struct s1 s2 env = 
  match env with
      (TyStructDef (s,lst,_))::rest when (s = s1) -> List.assoc s2 lst
    | _::rest -> gettype_struct s1 s2 rest
    | [] -> raise Not_found

(** Returns the integer index of field s2 in struct s1 as determined by environment env *)
let rec getfield_struct s1 s2 env = 
  let t_field = gettype_struct s1 s2 env in
  match env with
      (TyStructDef (s,lst,_))::rest when (s = s1) -> indexof (s2,t_field) lst 0
    | _::rest -> getfield_struct s1 s2 rest
    | [] -> raise Not_found

(** Returns the (string * bmdtype) list of struct fields of struct s1.  Raises Not_found if s1 is not defined. *)
let rec getfields_struct s1 env =
  match env with
      (TyStructDef (s,lst,_))::rest when (s = s1) -> lst
    | _::rest -> getfields_struct s1 rest
    | [] -> raise Not_found

(** Gets the number of fields of struct sname from the environment.  Raises Not_found if sname is not defined. *)
let rec nfields_struct sname env =
  match env with
      (TyStructDef (s,lst,_))::rest when (s = sname) -> List.length lst
    | _::rest -> nfields_struct sname rest
    | [] -> raise Not_found

(** Gets the AST data types of the struct fields *) 
let rec get_struct_data_type_list sname env =
  match env with 
      (TyStructDef (s,_,dtlist))::rest when (s = sname) -> dtlist
    | _::rest -> get_struct_data_type_list sname rest
    | [] -> raise Not_found


(** Annotates the data type AST element with information that will be needed by the IR *)
let annotate_data_type dt env = match dt with
    Ast.SynEnumOrStructType (s,tlist) ->
      if isdef_struct s env then 
	tlist := get_struct_data_type_list s env
      else if isdef_enum s env then
	tlist := [] 
      else 
	raise_type_error [s;" is not an enum or struct type but was used in a struct declaration."]
  | x -> ()
      
(** Creates a list of AST data type elements correlated with the struct fields *)
let rec make_struct_type_list varlist env = match varlist with
    (Ast.SynVarDeclareNoInit (dt,_))::rest ->
      let _ = annotate_data_type dt env in 
	dt::(make_struct_type_list rest env)
  | _::rest -> raise_type_error ["Struct declaration cannot contain a field initialization."]
  | [] -> []

(** Gives a string representation of the given type *)
let rec string_of_type ty = match ty with
    TyInt -> "int"
  | TyFloat -> "float"
  | TyString -> "string"
  | TyBool -> "bool"
  | TyVoid -> "void"
  | TyList t_list -> String.concat "" ["list(";string_of_type t_list;")"]
  | TyEnumType s -> String.concat "" ["enumType(";s;")"]
  | TyEnum s -> String.concat "" ["enum(";s;")"]
  | TyStruct s -> String.concat "" ["struct(";s;")"]
  | TyArray (t_arr) -> String.concat "" ["array(";string_of_type t_arr;")"]
  | TyArrow (t_args,t_ret) -> 
      let arg_string = List.map string_of_type t_args in
      let arg_string = if arg_string = []  then ["void"] else arg_string in
      String.concat " "
	(["function("]@arg_string@["->";string_of_type t_ret;")"])
  | TyRef t_ref -> String.concat "" ["ref(";string_of_type t_ref;")"]
  | TyTemplate s -> if s = "_unknown" then "_" else String.concat "" ["templated(";s;")"]

(** Gives an expanded string representation of the given enum type detailing its total number of values *)
let rec expanded_string_of_enum ename env =
  let nfields = get_nvalues_enum ename env in
    concat [ename;" : ";string_of_int nfields]

(** Gives an expanded string representation of the given struct type which includes the expanded representation
    of each of its fields *)
and expanded_string_of_struct sname env = 
  let struct_dts = get_second (getfields_struct sname env) in
  let helper x = x env in
  let string_list = List.map helper (List.map expanded_string_of_type struct_dts) in
    concat [sname;" : ";concat_commas string_list]
    
(** Gives a string representation of the given type with expanded represnetations of structs and enums *)
and expanded_string_of_type ty env = match ty with
    TyInt -> "int"
  | TyFloat -> "float"
  | TyString -> "string"
  | TyBool -> "bool"
  | TyVoid -> "void"
  | TyList t_list -> concat ["list(";expanded_string_of_type t_list env;")"]
  | TyEnumType s -> concat ["enumType(";expanded_string_of_enum s env;")"]
  | TyEnum s -> concat ["enum(";expanded_string_of_enum s env;")"]
  | TyStruct s -> concat ["struct(";expanded_string_of_struct s env;")"]
  | TyArray (t_arr) -> concat["array(";expanded_string_of_type t_arr env;")"]
  | TyArrow (t_args,t_ret) -> 
      let helper x = x env in
      let arg_string = List.map helper (List.map expanded_string_of_type t_args) in
      let arg_string = if arg_string = []  then ["void"] else arg_string in
      String.concat " "
	(["function("]@arg_string@["->";expanded_string_of_type t_ret env;")"])
  | TyRef t_ref -> concat ["ref(";expanded_string_of_type t_ref env;")"]
  | TyTemplate s -> if s = "_unknown" then "_" else concat ["templated(";s;")"]

(** Checks if op is a valid operator on values of type t1 and t2 *)
let rec isallowed_binop op t1 t2 = match (op,t1,t2) with
    (Ast.SynBinopAnd, TyBool, TyBool) -> true
  | (Ast.SynBinopOr, TyBool, TyBool) -> true
  | (Ast.SynBinopAdd, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynBinopSub, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynBinopMul, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynBinopDiv, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynBinopIDiv, TyInt, TyInt) -> true
  | (Ast.SynBinopMod, TyInt, TyInt) -> true
  | (Ast.SynBinopConcat, TyString, TyString) -> true
  | (Ast.SynBinopConcat, TyList t_list1, TyList t_list2) -> (t_list1 = t_list2)
  | (Ast.SynBinopCons, t_item, TyList t_list) -> (t_item = t_list)
  | (Ast.SynCompLt, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynCompGt, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynCompLte, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynCompGte, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynCompEq, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynCompNeq, (TyInt | TyFloat), (TyInt | TyFloat)) -> true
  | (Ast.SynCompEq, x, y) when (x = y) -> true
  | (Ast.SynCompNeq, x, y) when (x = y) -> true
  | _ -> false

(** Gets the type of the result of operator op on values of type t1 and t2.  Type error if invalid operator. *)
let rec gettype_binop op t1 t2 = match (op,t1,t2) with
    (Ast.SynBinopAnd, TyBool, TyBool) -> TyBool
  | (Ast.SynBinopOr, TyBool, TyBool) -> TyBool
  | (Ast.SynBinopAdd, TyInt, TyInt) -> TyInt
  | (Ast.SynBinopSub, TyInt, TyInt) -> TyInt
  | (Ast.SynBinopMul, TyInt, TyInt) -> TyInt
  | (Ast.SynBinopDiv, TyInt, TyInt) -> TyFloat
  | (Ast.SynBinopIDiv, TyInt, TyInt) -> TyInt
  | (Ast.SynBinopMod, TyInt, TyInt) -> TyInt
  | (Ast.SynBinopAdd, (TyInt | TyFloat), TyFloat) -> TyFloat
  | (Ast.SynBinopAdd, TyFloat, TyInt) -> TyFloat
  | (Ast.SynBinopSub, (TyInt | TyFloat), TyFloat) -> TyFloat
  | (Ast.SynBinopSub, TyFloat, TyInt) -> TyFloat
  | (Ast.SynBinopMul, (TyInt | TyFloat), TyFloat) -> TyFloat
  | (Ast.SynBinopMul, TyFloat, TyInt) -> TyFloat
  | (Ast.SynBinopDiv, (TyInt | TyFloat), TyFloat) -> TyFloat
  | (Ast.SynBinopDiv, TyFloat, TyInt) -> TyFloat
  | (Ast.SynBinopConcat, TyString, TyString) -> TyString
  | (Ast.SynBinopConcat, TyList t_list1, TyList t_list2) when (t_list1 = t_list2) -> TyList t_list1
  | (Ast.SynBinopCons, t_item, TyList t_list) when (t_item = t_list) -> TyList t_list
  | (Ast.SynCompLt, (TyInt | TyFloat), (TyInt | TyFloat)) -> TyBool
  | (Ast.SynCompGt, (TyInt | TyFloat), (TyInt | TyFloat)) -> TyBool
  | (Ast.SynCompLte, (TyInt | TyFloat), (TyInt | TyFloat)) -> TyBool
  | (Ast.SynCompGte, (TyInt | TyFloat), (TyInt | TyFloat)) -> TyBool
  | (Ast.SynCompEq, (TyInt | TyFloat), (TyInt | TyFloat)) -> TyBool
  | (Ast.SynCompNeq, (TyInt | TyFloat), (TyInt | TyFloat)) -> TyBool
  | (Ast.SynCompEq, x, y) when (x = y) -> TyBool
  | (Ast.SynCompNeq, x, y) when (x = y) -> TyBool
  | _ -> raise_type_error ["Attempted to perform an invalid binary operation ";Ast.string_of_binop op;" on values of type ";string_of_type t1;" and ";string_of_type t2]

(** Types a list of AST data type element *)
let rec type_data_types dt_lst env =
  match dt_lst with
      Ast.SynIntType::rest -> TyInt::(type_data_types rest env)
    | Ast.SynFloatType::rest -> TyFloat::(type_data_types rest env)
    | Ast.SynStringType::rest -> TyString::(type_data_types rest env)
    | Ast.SynBoolType::rest -> TyBool::(type_data_types rest env)
    | Ast.SynVoidType::rest -> TyVoid::(type_data_types rest env)
    | (Ast.SynEnumOrStructType (s,field_types))::rest ->
	if isdef_struct s env then 
	  let _ = field_types  := get_struct_data_type_list s env in
	    (TyStruct s)::(type_data_types rest env)
	else if isdef_enum s env then 
	  (TyEnum s)::(type_data_types rest env)
	else if isdef_templated s env then
	  (TyTemplate s)::(type_data_types rest env)
	else raise_type_error ["Invalid data type: ";s;" expected enum, struct, or templated type."]
    | (Ast.SynArrayType (dt,n))::rest ->
	let t_n,_ = type_value_producer n env in
	  if t_n = TyInt then 
	    (TyArray (type_data_type dt env))::(type_data_types rest env)
	  else
	    raise_type_error ["Invalid array index type ";string_of_type t_n;" expected an integer value."]
    | (Ast.SynListType dt)::rest ->
	(TyList (type_data_type dt env))::(type_data_types rest env)
    | (Ast.SynRefType dt)::rest ->
	(TyRef (type_data_type dt env))::(type_data_types rest env)
    | (Ast.SynArrowType (arglist_types,ret_type))::rest ->
	let arglist_types = type_data_types arglist_types env in
	  (TyArrow ((if arglist_types = [] then [TyVoid] else arglist_types), 
		    type_data_type ret_type env))::(type_data_types rest env)
    | [] -> []

(** Types a single AST data type element *)
and type_data_type dt env = 
  let verified_type_lst = type_data_types [dt] env in
    match verified_type_lst with 
	x::rest -> x
      | [] -> raise_type_error ["Unexpected type error."]

(** Types the declaration of a list of struct fields.  Initial values not allowed, type error if they are. *)
and structdef_list lst env = match lst with
    (Ast.SynVarDeclareNoInit (dt,s))::[] -> [s,type_data_type dt env]
  | (Ast.SynVarDeclareNoInit (dt,s))::rest -> (s,type_data_type dt env)::(structdef_list rest env)
  | _ -> raise_type_error ["Expected uninitialized variable declaration in struct definition."] 

(** Types an argument list to a function, adding the arguments as variable declarations to the environment. *)
and vardef_arglist arglist env = 
  match arglist with 
      (dt,name)::rest -> vardef_arglist rest ((TyVarDef (name,type_data_type dt env))::env)
    | [] -> env

(** Types a value *)
and type_value v env = match v with
    Ast.SynIntValue _ -> TyInt, env
  | Ast.SynFloatValue _ -> TyFloat, env
  | Ast.SynBoolValue _ -> TyBool, env
  | Ast.SynStringValue _ -> TyString, env

(** Produces a comma-separated string representation of a list of types *)
and string_of_type_list lst = match lst with
    t::[] -> string_of_type t
  | t::rest -> String.concat "" [string_of_type t;", ";string_of_type_list rest]
  | [] -> ""

(** Checks if association list l1 has no conflicting key/value pairs with association list l2 *)
and are_assoclists_compatible l1 l2 = match l1 with
    (x,y)::rest -> 
      if List.mem_assoc x l2 
      then y = List.assoc x l2
      else are_assoclists_compatible rest l2
  | [] -> true

(** Returns an association list of template types to bmdtypes.  Type error if t2 cannot be unified to t1. *)
and unify_types t1 t2 = match t1,t2 with
    x,y when x = y -> []
  | TyList t_list1, TyList t_list2 -> unify_types t_list1 t_list2
  | TyArray t_arr1, TyArray t_arr2 -> unify_types t_arr1 t_arr2
  | TyArrow (args1,ret1), TyArrow (args2,ret2) -> 
      let assoc1 = unify_types ret1 ret2 in
      let assoc2 = List.flatten (List.map2 unify_types args1 args2) in
	if are_assoclists_compatible assoc1 assoc2 then
	  assoc1 @ assoc2
	else 
	  raise_type_error ["Could not unify templated arrow type ";
			    string_of_type t2;
			    " with ";
			    string_of_type t1]
  | TyRef t_ref1, TyRef t_ref2 -> unify_types t_ref1 t_ref2
  | TyTemplate s, t2 -> [(s,t2)]
  | _ -> raise_type_error ["Expected type ";
			   string_of_type t1;
			   " but got type ";
			   string_of_type t2]

(** Uses the association list (substitution list) template_map (key: template string, value: bmdtype)
    to type the given data type with those substitutions *)
and insert_template_types template_map env dt = 
  let insert_types = insert_template_types template_map env in
    match dt with
	TyList t -> TyList (insert_types t)
      | TyArray t -> TyArray (insert_types t)
      | TyArrow (args,ret) -> TyArrow (List.map insert_types args, insert_types ret)
      | TyRef t -> TyRef (insert_types t)
      | TyTemplate s -> 
	  if List.mem_assoc s template_map then
	    List.assoc s template_map
	  else if isdef_templated s env then
	    TyTemplate s
	  else 
	    raise_type_error ["Expression has free template type ";string_of_type dt;" in result."]
      | x -> x

(** Apply argtypes to the arrow type, making sure there are no 
   template inconsistencies, and get the return type *)
and apply_arrow_type arrowtype argtypes env = 
  let lhs_types, ret_type = match arrowtype with
      TyArrow (x,y) -> x,y
    | _ -> raise_type_error ["Attempted to apply arguments to something of non-arrow type ";string_of_type arrowtype;"."]
  in
  let isVoidArgs = lhs_types = [TyVoid] && (List.length argtypes = 0) in
  let _ = 
    if (not isVoidArgs) && ((List.length lhs_types) != (List.length argtypes))
    then raise_type_error ["Attempted to apply an incorrect number of arguments to arrow type ";string_of_type arrowtype;"."]
    else ()
  in
  let template_map = ref [] in
  let _ = 
    if isVoidArgs then () else
      for i = 0 to (List.length lhs_types)-1 do
	let cur_lhs, cur_arg = List.nth lhs_types i, List.nth argtypes i in
	let unify_result = unify_types cur_lhs cur_arg in
	  if are_assoclists_compatible unify_result !template_map then
	    template_map := unify_result @ !template_map
	  else
	    raise_type_error ["Could not apply argument types ";
			      string_of_type_list argtypes;
			      " to a function of type ";
			      string_of_type arrowtype]
      done
  in
    insert_template_types !template_map env ret_type
      
(** Types a function call as a value-producer (checks argument types, gives the return type).
    Updates the linker header with inferred remote call types. *)
and type_function_call_value funcall env = match funcall with
    Ast.SynLocalCall (s,vprod_list,isvar) -> 
      if isdef_fun s env || isdef_var s env then
	let fun_type = 
	  if isdef_fun s env then 
	    gettype_fun s env 
	  else 
	    let _ = isvar := true in
	      gettype_var s env 
	in
	let vplist_types,_ = type_value_producer_list_nonuniform vprod_list env in
	  try
	    apply_arrow_type fun_type vplist_types env, env
	  with
	      Type_error err_string ->
		raise_type_error ["Call to (local) function ";s;" was malformed. ";err_string]
      else 
	raise_type_error ["(Local) function call to undefined function ";s;"."]
  | Ast.SynRemoteCall (s,vprod_list, var_ident) -> (** the linker must do most of the work here **)
      let argtypes,_ = type_value_producer_list_nonuniform vprod_list env in
      let rettype,_ = type_variable_identifier var_ident env in
      let _ = append_link_buffer (concat ["require ";s;" = ";expanded_string_of_type (TyArrow (argtypes,rettype)) env;"\n";]) in
	TyBool, env (** the result is true or false depending on call success/failure **)
  | Ast.SynRemoteCallNoResult (s,vprod_list) -> (** again, the linker will do most of the work **)
      let argtypes,_ = type_value_producer_list_nonuniform vprod_list env in
      let rettype = TyTemplate ("_unknown") in
      let _ = append_link_buffer (concat ["require ";s;" = ";expanded_string_of_type (TyArrow (argtypes,rettype)) env;"\n";]) in
	TyBool, env
	
(** Types a binary operation vp1 op vp2.  Type error if not a valid operation *)
and type_binop vp1 op vp2 env =
  let t_vp1,_ = type_value_producer vp1 env in
  let t_vp2,_ = type_value_producer vp2 env in
    if isallowed_binop op t_vp1 t_vp2 then
      (gettype_binop op t_vp1 t_vp2), env
    else 
      raise_type_error ["Attempted to perform an invalid binary operation ";Ast.string_of_binop op;" on values of type ";string_of_type t_vp1;" and ";string_of_type t_vp2;"."]
    
(** Types a unary operation expression. Type error if not a valid operation. *)
and type_prefix_unop op vp env = 
  let t_vp,_ = type_value_producer vp env in
    match (op,t_vp) with 
	Ast.SynUnopNot, TyBool -> TyBool, env
      | Ast.SynUnopCar, TyList t_list -> t_list, env
      | Ast.SynUnopCdr, TyList t_list -> TyList t_list, env
      | Ast.SynUnopTrunc, (TyFloat | TyInt) -> TyInt, env
      | Ast.SynUnopDeref, TyRef t_ref -> t_ref, env
      | Ast.SynUnopNeg, TyInt -> TyInt, env
      | Ast.SynUnopNeg, TyFloat -> TyFloat, env
      | Ast.SynUnopNull, _ -> TyBool, env
      | _ -> raise_type_error ["Attempted to perform an invalid prefix unary operation on a value of type ";string_of_type t_vp;"."]

(** Types an array value producer (i.e. {elem1, elem2...}.  Error if empty (we need to infer the type),
    or if values of different types are used. *)
and type_array_value_producer vp_list env = match vp_list with
    vp::[] ->
      let vp_type,_ = type_value_producer vp env in
	TyArray vp_type, env
  | vp::rest ->
      let vp_type,_ = type_value_producer vp env in
      let rest_type,_ = type_array_value_producer rest env in
	(match rest_type with
	     TyArray rest_arr_type when rest_arr_type = vp_type ->
	       TyArray vp_type, env
	   | _ -> raise_type_error ["An element of type ";string_of_type vp_type;" is being used with an array of type ";string_of_type rest_type;"."])
  | [] -> raise_type_error ["Constant array values must be non-empty."]

(** Types a list of value producers (i.e. [elem1; elem2;...])  Error if empty or 
    if values of different types are used. *)
and type_value_producer_list vp_list env = match vp_list with
    vp::[] ->
      let vp_type,_ = type_value_producer vp env in
	TyList vp_type, env
  | vp::rest ->
      let vp_type,_ = type_value_producer vp env in
      let rest_type,_ = type_value_producer_list rest env in
	if TyList vp_type = rest_type then
	  TyList vp_type, env
	else 
	  raise_type_error ["An element of type ";string_of_type vp_type;" is being used with list of type ";string_of_type rest_type;"."]
  | [] -> raise_type_error ["Constant list values must be non-empty."]

(** Types a list value producer *)
and type_list_value_producer vp_list env = match vp_list with
    Ast.SynListNil dt -> 
      let t_dt = type_data_type dt env in
	TyList t_dt, env
  | Ast.SynListList vp_list ->
      type_value_producer_list vp_list env
  | Ast.SynListCons (vp1,vp2) ->
      let t_vp1,_ = type_value_producer vp1 env in
      let t_vp2,_ = type_value_producer vp2 env in
	(match t_vp2 with 
	    TyList t_list when t_list = t_vp1 -> t_vp2, env
	  | _ -> raise_type_error ["Cannot cons a value of type ";string_of_type t_vp1;" to a list of type ";string_of_type t_vp2;"."])

(** Types a value producer *)
and type_value_producer vp env = match vp with
    Ast.SynValue v -> type_value v env
  | Ast.SynFunctionCallValue funcall -> type_function_call_value funcall env
  | Ast.SynVarIdentifier vident -> type_variable_identifier vident env 
  | Ast.SynBinop (vp1,op,vp2) -> type_binop vp1 op vp2 env
  | Ast.SynPrefixUnop (op,vp) -> type_prefix_unop op vp env
  | Ast.SynParenthesized vp -> type_value_producer vp env
  | Ast.SynArrayValueProducer vp_list -> type_array_value_producer vp_list env
  | Ast.SynListValueProducer vp_list -> type_list_value_producer vp_list env

(** Returns a list of types, i.e. for parameters *)
and type_value_producer_list_nonuniform vp_list environment =  
  let helper vplist env = 
    match vplist with
	vp::rest -> 
	  let t_vp,_ = type_value_producer vp env in
	  let t_rest,_ = type_value_producer_list_nonuniform rest env in
	    t_vp::t_rest
      | [] -> []
  in
    helper vp_list environment, environment

(** Types a variable identifier *)
and type_variable_identifier v_ident env = match v_ident with
    Ast.SynVarName s -> 
      if isdef_var s env then
	gettype_var s env, env
      else if isdef_enum s env then
	TyEnumType s, env
      else if isdef_struct s env then
	TyStruct s, env
      else if isdef_fun s env then
	gettype_fun s env, env
      else
	raise_type_error ["Variable ";s;" was used but not defined."]
  | Ast.SynStructOrEnumValue (vid1,s2,field_info) ->
      let t_vid,_ = type_variable_identifier vid1 env in
	(match t_vid with
	     TyEnumType s1 -> 
	       if is_enum_value s1 s2 env then
		 let index = get_enum_value s1 s2 env in
		 let _ = field_info := (Ast.VIdEnumValue index) in
		   TyEnum s1, env
	       else 
		 raise_type_error [s1;".";s2;" is not an enum value of enum type ";s1;"."]
	   | TyStruct s1 ->
	       if isdef_struct s1 env then
		 if is_struct_value s1 s2 env then
		   let index = getfield_struct s1 s2 env in
		   let _ = field_info := (Ast.VIdStructField index) in
		   gettype_struct s1 s2 env, env
		 else
		   raise_type_error [s1;".";s2;" refers to a non-existent field of struct type ";string_of_type t_vid;"."]
	       else
		 raise_type_error [s1;" is used as a struct but is not a struct."]
	   | _ -> raise_type_error ["An identifier of type ";string_of_type t_vid;" is not a struct or enum type in this scope."])
  | Ast.SynArrayCell (vp,i) -> 
      let t,_ = type_variable_identifier vp env in
      let t_index,_ = type_value_producer i env in
	(match (t,t_index) with
	     TyArray t_arr, TyInt ->
	       t_arr, env
	   | _ -> raise_type_error ["A value of non-array-type ";string_of_type t;" is used as an array type."])	    

(** Types a variable assignment *)
let type_var_assign vasgn env = match vasgn with 
    Ast.SynVarAssignment (var,vprod) ->
      let var_type, temp_env = type_variable_identifier var env in
      let vp_type, new_env = type_value_producer vprod temp_env in
	if vp_type = var_type or TyRef vp_type = var_type then 
	  TyVoid, new_env
	else 
	  raise_type_error ["Cannot assign a value of type ";string_of_type vp_type;" to something of type ";string_of_type var_type;"."]
  | Ast.SynVarModify (vident, op, vprod) -> 
      let var_type, temp_env = type_variable_identifier vident env in
      let vp_type, new_env = type_value_producer vprod temp_env in
      let t1, t2 = 
	(match op with 
	     Ast.SynBinopCons -> vp_type, var_type
	   | _ -> var_type, vp_type) (** flip order for cons, the list is being mutated **)
      in
	if isallowed_binop op t1 t2 then
	  TyVoid, new_env
	else 
	  raise_type_error ["Cannot use operator ";Ast.string_of_binop op;" to mutate a value of type ";string_of_type var_type;" with an argument of type ";string_of_type vp_type;"."]

(** Types a variable declaration *)
let type_var_declaration vdecl env = match vdecl with
    Ast.SynVarDeclareNoInit (dt,s) ->
      if isdef_ident s env then 
	raise_type_error ["Cannot declare ";s;" as a variable; it has already been declared."]
      else
	let new_dt = type_data_type dt env in
	  (match new_dt with
	       TyTemplate s -> raise_type_error ["Cannot declare templated variable ";s;" without explicit initialization."]
	     | _ ->
		 let new_env = (TyVarDef (s,new_dt))::env in
		   TyVoid, new_env)
  | Ast.SynVarDeclareWithInit (dt,s,vp) ->
      if isdef_ident s env then
	raise_type_error ["Cannot declare ";s;" as a variable; it has already been declared."]
      else
	let new_dt = type_data_type dt env in
	let temp2_env = (TyVarDef (s,new_dt))::env in
	let vp_dt,new_env = type_value_producer vp temp2_env in
	  if vp_dt = new_dt then
	    TyVoid, new_env
	  else 
            raise_type_error ["Variable ";s;" has type ";string_of_type new_dt;" but is being initialized with a value of type ";string_of_type vp_dt;"."]

(** Types a conditional expression *)
let rec type_conditional cond env rettype = match cond with 
    Ast.SynIf (vprod,expr_list) ->
      let vp_type, new_env = type_value_producer vprod env in
	if vp_type = TyBool then
	  let body_type,_ = type_expression_list expr_list env rettype in
	    if body_type = TyVoid then
	      TyVoid, new_env
	    else 
	      raise_type_error ["Conditional body was not well-formed."]
	else
	  raise_type_error ["Conditional test expression must have a boolean type."]
  | Ast.SynIfCase (vprod,expr_list,continued_cond) ->
      let vp_type, new_env = type_value_producer vprod env in
	if vp_type = TyBool then
	  let body_type,_ = type_expression_list expr_list env rettype in
	    if body_type = TyVoid then
	      type_continued_conditional continued_cond new_env rettype
	    else 
	      raise_type_error ["Conditional case body was not well-formed."]
	else 
	  raise_type_error["Conditional case test expression must have a boolean type."]

(** Types an else case *)
and type_continued_conditional continued_cond env rettype = match continued_cond with
    Ast.SynFinalElse expr_list -> 
      let body_type,_ = type_expression_list expr_list env rettype in
	if body_type = TyVoid then
	  TyVoid, env
	else
	  raise_type_error ["Final else body was not well-formed."]
  | Ast.SynElse cond -> type_conditional cond env rettype

(** Types a loop *)
and type_loop loop env rettype = match loop with
    Ast.SynWhile (vprod, loop_expr_list) -> 
      let vp_type, new_env = type_value_producer vprod env in
	(match vp_type with
	     TyBool ->
	       let body_type,_ = type_expression_list loop_expr_list env rettype in
		 (match body_type with
		      TyVoid -> TyVoid, new_env
		    | _ -> raise_type_error ["While loop body was not well-formed."])
	   | _ -> raise_type_error ["While loop test expression must have a boolean type."])
  | Ast.SynDoWhile (loop_expr_list, vprod) -> 
      let vp_type, new_env = type_value_producer vprod env in
	(match vp_type with
	     TyBool ->
	       let body_type,_ = type_expression_list loop_expr_list env rettype in
		 (match body_type with
		      TyVoid -> TyVoid, new_env
		    | _ -> raise_type_error ["While loop body was not well-formed."])
	   | _ -> raise_type_error ["While loop test expression must have a boolean type."])
  | Ast.SynFor (init_expr, test_vprod, step_expr, loop_expr_list) ->
      let _, init_env = type_expression init_expr env rettype in
      let vp_type, _ = type_value_producer test_vprod init_env in
      let _ = type_expression step_expr init_env rettype in
	(match vp_type with
	     TyBool ->
	       let _ = type_expression step_expr in (** we dont care about the type as long as it has one **)
	       let body_type,_ = type_expression_list loop_expr_list init_env rettype in
		 (match body_type with
		      TyVoid -> TyVoid, env
		    | _ -> raise_type_error ["For loop body was not well-formed."])
	   | _ -> raise_type_error ["For loop test expression must have a boolean type."])

(** Types a function call expression.  This is not a value producer, so regardless of return type 
   the expression will have type void *)
and type_function_call funcall env rettype = match funcall with
    Ast.SynLocalCall (s,vprod_list,isvar) -> 
     if isdef_fun s env || isdef_var s env then
	let fun_type = 
	  if isdef_fun s env then 
	    gettype_fun s env 
	  else 
	    let _ = isvar := true in
	      gettype_var s env 
	in
	
	let vplist_types,_ = type_value_producer_list_nonuniform vprod_list env in
	  try
	    let _ = apply_arrow_type fun_type vplist_types env in
	      TyVoid, env
	  with
	      Type_error err_string ->
		raise_type_error ["Call to (local) function ";s;" was malformed. ";err_string]
     else 
	raise_type_error ["(Local) function call to undefined function ";s;"."]
  | Ast.SynRemoteCall (s,vprod_list, var_ident) -> (** the linker must do most of the work here **)
      let argtypes,_ = type_value_producer_list_nonuniform vprod_list env in
      let rettype,_ = type_variable_identifier var_ident env in
      let _ = append_link_buffer (concat ["require ";s;" = ";expanded_string_of_type (TyArrow (argtypes,rettype)) env;"\n"]) in
	TyVoid, env
  | Ast.SynRemoteCallNoResult (s,vprod_list) -> (** again, the linker will do most of the work **)
      let argtypes,_ = type_value_producer_list_nonuniform vprod_list env in
      let rettype = TyTemplate "_unknown" in
      let _ = append_link_buffer (concat ["require ";s;" = ";expanded_string_of_type (TyArrow (argtypes,rettype)) env;"\n"]) in
	TyVoid, env

(** Types an expression.  We use void to indicate a successful typing, since expressions have no 
    actual type. *)
and type_expression expr env rettype = match expr with
    Ast.SynVarDeclare vdecl -> type_var_declaration vdecl env
  | Ast.SynEnumDeclare (s, slist) ->
      if isdef_ident s env then
	raise_type_error ["Cannot declare ";s;" as a variable; it has already been declared."]
      else 
	TyVoid, (TyEnumDef (s,slist))::env
  | Ast.SynVarAssign vasgn -> type_var_assign vasgn env
  | Ast.SynCond cond -> type_conditional cond env rettype
  | Ast.SynLoop loop -> type_loop loop env rettype
  | Ast.SynFunctionCall funcall -> type_function_call funcall env rettype
  | Ast.SynReturnStatement ret -> 
      (match ret with
	   Ast.SynVoidReturn -> 
	     if rettype = TyVoid then
	       TyVoid, env
	     else 
	       raise_type_error ["Invalid return statement: expected a value of type void but received one of type ";string_of_type rettype;"."]
	 | Ast.SynValueReturn vprod ->
	     let vp_type, new_env = type_value_producer vprod env in
	       if vp_type = rettype then
		 TyVoid, new_env
	       else 
		 raise_type_error ["Invalid return statement: expected a value of type ";string_of_type rettype;" but received one of type ";string_of_type vp_type;"."])
  | Ast.SynBreak -> TyVoid, env
  | Ast.SynContinue -> TyVoid, env

(** Types an expression list.  Checks if all expressions are validly (void) typed, and then returns
    void type.  Otherwise raises type errors *)
and type_expression_list lst env rettype = match lst with
    expr::rest -> 
      let ty,new_env = type_expression expr env rettype in
	(match ty with
	     TyVoid -> type_expression_list rest new_env rettype
	   | _ -> raise_type_error ["Invalid expression encountered."])
  | [] -> TyVoid, env

(** Types a program (top-level expression list *)
and type_program p env = match p with
    expr::rest -> 
      let t,new_env = type_global_expression expr env in
	(match t with
	     TyVoid -> type_program rest new_env
	   | _ -> raise_type_error ["Global expression was not well formed (non-void type)."])
  | [] -> TyVoid, env

(** Types a top level expression *)
and type_global_expression ast env = 
  match ast with
      Ast.SynFunctionDeclare (dt,lbl,arg_list) ->
	if isdef_ident lbl env then
	  raise_type_error ["Function ";lbl;" is defined more than once."]
	else
	  let fun_type = 
	    let arglist_types = type_data_types (get_first arg_list) env in
	    TyArrow ((if arglist_types = [] then [TyVoid] else arglist_types), 
		     type_data_type dt env) in
	  let new_env = (TyFunDef (lbl,fun_type))::env in
	    TyVoid, new_env
    | Ast.SynRemotableFunctionDeclare (dt,lbl,arg_list) ->
	let fun_type = 
	  let arglist_types = type_data_types (get_first arg_list) env in
	    TyArrow ((if arglist_types = [] then [TyVoid] else arglist_types), 
		     type_data_type dt env) 
	in
	let _ = append_link_buffer (concat ["provide ";lbl;" = ";expanded_string_of_type fun_type env;"\n"]) in
	let new_env = (TyFunDef (lbl,fun_type))::env in
	  TyVoid, new_env
    | Ast.SynFunctionDefine ((dt,lbl,arg_list),expr_list) ->
	let ret_type = type_data_type dt env in
	let fun_type =
	  let arglist_types = type_data_types (get_first arg_list) env in
	    TyArrow ((if arglist_types = [] then [TyVoid] else arglist_types),
		     ret_type) in
	let new_env = (TyFunDef (lbl,fun_type))::env in
	let body_env = vardef_arglist arg_list new_env in
	let body_type,_ = type_expression_list expr_list body_env ret_type in
	  (match body_type with 
	      TyVoid -> TyVoid, new_env
	    | _ -> raise_type_error ["Function body had an invalid type."])
    | Ast.SynTemplatedDeclare (typelist, expr) ->
	let new_env = (TyTemplateDef typelist)::env in
	let result_type,result_env = type_global_expression expr new_env in
	  result_type, remove_template_def result_env
    | Ast.SynTemplatedDefine (typelist, expr) ->
	let new_env = (TyTemplateDef typelist)::env in
	let result_type,result_env = type_global_expression expr new_env in
	  result_type, remove_template_def result_env
    | Ast.SynStructDeclare (s,varlist) -> 
	let typed_varlist = structdef_list varlist env in
	let type_list = make_struct_type_list varlist env in
	let new_env = (TyStructDef (s, typed_varlist, type_list))::env in
	  TyVoid, new_env
    | Ast.SynGlobalEnumDeclare (s, lst) ->
	TyVoid, (TyEnumDef (s,lst))::env
    | Ast.SynGlobalVarDeclare vdecl -> 
	(match vdecl with
	     Ast.SynVarDeclareNoInit (dt, s) ->
	       let new_dt = type_data_type dt env in
	       let new_env = (TyVarDef (s,new_dt))::env in
		 TyVoid, new_env
	   | _ -> raise_type_error ["Global variable declarations cannot be initialized."])
    | Ast.SynInclude data ->
	(** If this is the first time processing this include, parses the include and inserts 
	    its AST after typechecking it.  If not, skips it. *)
	(match !data with
	     Ast.IncludeFileName s ->
	       if isdef_include s env then
		 let _ = begin
		   data := (Ast.IncludeAst []);
		   print_string "Skipping repeat include ";
		   print_string s;
		   print_string ".\n";
		   flush stdout;
		 end
		 in
		   TyVoid, env
	       else
		 (let ast = ref [] in
		    try
		      let inchan = open_in s in
		      let lexbuf = Lexing.from_channel inchan in
			begin
			  ast := Bmdparse.program Bmdlex.bmd_lang lexbuf;
			  print_string "Type checking include ";
			  print_string s;
			  print_string ".\n";
			  flush stdout;
			  let t,new_env = type_program !ast env in
			    (match t with 
				 TyVoid -> 
				   let _ = data := (Ast.IncludeAst !ast) in
				     TyVoid, (TyIncludeDef s)::new_env
			       | _ -> raise_type_error ["Typechecking of include file ";s;" failed.\n"])
			end
		    with 
			Parsing.Parse_error -> raise_type_error ["Syntax error in include file ";s;" line ";string_of_int !Bmdlex.line_count;".\n"])
	   | _ ->
	       raise_type_error ["Unexpected type error: include should be specified by file name, not as an ast."])
    | Ast.SynStateMachine _ ->
	(** State machine declarations should be eliminated from code during source-to-source compilation. *)
	raise_type_error ["Unexpected type error: source-to-source compilation of state machine failed."]

(** Determines whether an AST is validly typed.  Prints typing exceptions. *)
let is_typed p = 
  try
    let t,_ = type_program p [] in
      match t with
	  TyVoid -> true
	| _ -> false
  with Type_error s -> 
    let _ = output_string stderr s in
    let _ = output_string stderr "\n" in
    let _ = flush stderr in
      false 

